using System;
using System.Collections.Generic;
using System.Text;

namespace SqlTools.CodeGenerator.Data
{
	public class ProjectMerger
	{
		protected Project _originalProject;
		protected Project _updatedProject;

		public ProjectMerger(Project originalProject, Project updateProject)
		{
			_originalProject = originalProject;
			_updatedProject = updateProject;
		}

		public Project OriginalProject
		{
			get { return _originalProject; }
		}

		public Project UpdatedProject
		{
			get { return _updatedProject; }
		}

		public void Merge()
		{
			// now do a comparison, and add columns as necessary
			// or add tables

			foreach ( Table table in _updatedProject.Tables )
			{
				Table tTable = _originalProject.Tables[table.TableName];

				if ( tTable == null )
				{
					// table doesn't exist yet, so add it.
					_originalProject.Tables.Add(table);

					// TODO: will need to check foreign key references

					table.Properties.OperateOnProperties(delegate(TableProperty tp)
					{
						if ( tp.Type == TablePropertyType.ForeignKey )
						{
							ForeignKeyTableProperty fktp = tp as ForeignKeyTableProperty;

							if ( fktp.ReferenceTable != table )
							{
								GetTableOrTemp(fktp.ReferenceTable);

								CopyMethod(fktp.ReferenceRetrieve, table);
							}
						}

					});
				}
				else
				{
					CheckTableProperties(table, tTable);
				}
			}

			// now loop through removing all temp tables and temp methods
			foreach ( Table table in _originalProject.Tables )
			{
				foreach ( TableProperty tp in table.Properties )
				{
					if ( tp.Type == TablePropertyType.ForeignKey )
					{
						ForeignKeyTableProperty fktp = tp as ForeignKeyTableProperty;

						if ( fktp.ForeignKeyTable is TempTable )
						{
							// find real table
							fktp.ForeignKeyTable = _originalProject.Tables[fktp.ForeignKeyTable.TableName];
						}

						foreach ( MethodParameter mp in fktp.ForeignRetrieve.Parameters )
						{
							if ( mp.Property is TempTableProperty )
							{
								mp.Property = fktp.ForeignKeyTable.Properties.FindPropertyByColumnName(mp.Property.ColumnName);
							}
						}

						if ( fktp.ReferenceTable is TempTable )
						{
							// find real table
							fktp.ReferenceTable = _originalProject.Tables[fktp.ReferenceTable.TableName];
						}

						foreach ( MethodParameter mp in fktp.ReferenceRetrieve.Parameters )
						{
							if ( mp.Property is TempTableProperty )
							{
								mp.Property = fktp.ForeignKeyTable.Properties.FindPropertyByColumnName(mp.Property.ColumnName);
							}
						}
					}
				}
			}

			DataHelper.ResolveFKIssues(_originalProject.Tables);
		}

		private void CheckTableProperties(Table sourceTable, Table targetTable)
		{
			// first check primary key
			PrimaryKeyTableProperty spktp = sourceTable.GetPrimaryKey();
			PrimaryKeyTableProperty tpktp = targetTable.GetPrimaryKey();

			#region PrimaryKey Checks
			if ( spktp == null && tpktp != null )
			{
				// primary key was removed
				// so see if the columns 
				// were removed, or the index
				foreach ( TableProperty tp in tpktp.Properties.ToArray() )
				{
					if ( sourceTable.Properties.FindPropertyByColumnName(tp.ColumnName) != null )
					{
						// index was removed from this column,
						// so move it to the normal table properties
						tpktp.Properties.Remove(tp);
						targetTable.Properties.Add(tp);
					}
				}

			}
			else if ( tpktp == null && spktp != null )
			{
				// primary key was added
				// so create new PK and 
				// see if existing columns
				// were added to the PK
				tpktp = new PrimaryKeyTableProperty(spktp.PropertyName, targetTable, spktp.ColumnName);
				tpktp.AutoGeneratedOnInsert = spktp.AutoGeneratedOnInsert;
				//tpktp.CSharpDataType = spktp.CSharpDataType;
				tpktp.DataLength = spktp.DataLength;
				tpktp.DataType = spktp.DataType;
				tpktp.DefaultValue = spktp.DefaultValue;
				tpktp.FieldName = spktp.FieldName;
				tpktp.Nullable = spktp.Nullable;

				foreach ( TableProperty tp in spktp.Properties.ToArray() )
				{
					if ( targetTable.Properties.FindPropertyByColumnName(tp.ColumnName) != null )
					{
						// index was removed from this column,
						// so move it to the normal table properties
						targetTable.Properties.Remove(tp);
						tpktp.Properties.Add(tp);
					}
				}
			}
			else if ( tpktp != null && spktp != null )
			{
				// check for changes in primary key
				// first find any columns removed
				#region Find Removed PK Columns
				foreach ( TableProperty tp in tpktp.Properties.ToArray() )
				{
					TableProperty stp = spktp.Properties.FindPropertyByColumnName(tp.ColumnName);
					if ( stp == null )
					{
						// not in primary key anymore
						tpktp.Properties.Remove(tp);

						// check if its in the table
						stp = sourceTable.Properties.FindPropertyByColumnName(tp.ColumnName);

						if ( stp != null )
						{
							// add to normal properties
							targetTable.Properties.Insert(1, tp);

							ConvergeTableProperties(targetTable, tpktp.Properties, tp, stp);
						}
					}
					else
					{
						ConvergeTableProperties(targetTable, tpktp.Properties, tp, stp);
					}
				}
				#endregion

				// then find new columns
				#region Find New PK Columns
				foreach ( TableProperty stp in spktp.Properties.ToArray() )
				{
					TableProperty tp = tpktp.Properties.FindPropertyByColumnName(stp.ColumnName);
					if ( tp == null )
					{
						// found a column added to the primary key

						// first check if the column exists, just not as a PK
						tp = targetTable.Properties.FindPropertyByColumnName(stp.ColumnName);

						if ( tp != null )
						{
							// column exists, so move into PK
							int index = targetTable.Properties.IndexOf(tp);

							if ( index > 0 ) index--;

							targetTable.Properties.Remove(tp);

							tpktp.Properties.Insert(index - 1, tp);

							ConvergeTableProperties(targetTable, tpktp.Properties, tp, stp);
						}
						else
						{
							// column does not exist yet

							if ( stp.Type == TablePropertyType.Intrinsic )
							{
								// just create a new intrinsic property
								IntrinsicTableProperty sitp = stp as IntrinsicTableProperty;

								IntrinsicTableProperty itp = CreateIntrinsicTableProperty(targetTable, sitp);

								tpktp.Properties.Insert(spktp.Properties.IndexOf(stp), itp);

							}
							else
							{
								ForeignKeyTableProperty fktp = CreateForeignKeyTableProperty(targetTable, stp as ForeignKeyTableProperty);

								tpktp.Properties.Insert(spktp.Properties.IndexOf(stp), fktp);
							}

						}
					}
					else
					{
						// now just resolve differences
						ConvergeTableProperties(targetTable, tpktp.Properties, tp, stp);
					}
				}
				#endregion
			}
			#endregion

			// at this point, the primary keys should be resolved,
			// so loop through the normal table properties
			#region Normal Column Checks
			#region Find Removed Columns
			foreach ( TableProperty tp in targetTable.Properties.ToArray() )
			{
				if ( tp.Type != TablePropertyType.PrimaryKey )
				{
					// check if column exists in source table
					TableProperty stp = sourceTable.Properties.FindPropertyByColumnName(tp.ColumnName);

					if ( stp != null )
					{
						// column exists
						ConvergeTableProperties(targetTable, targetTable.Properties, tp, stp);
					}
					else
					{
						// column has been removed
						targetTable.Properties.Remove(tp);
					}
				}
			}
			#endregion

			#region Find New Columns
			foreach ( TableProperty stp in sourceTable.Properties.ToArray() )
			{
				if ( stp.Type != TablePropertyType.PrimaryKey )
				{
					TableProperty tp = targetTable.Properties.FindPropertyByColumnName(stp.ColumnName);

					if ( tp == null )
					{
						// new column
						if ( stp.Type == TablePropertyType.Intrinsic )
						{
							IntrinsicTableProperty itp = CreateIntrinsicTableProperty(targetTable, stp as IntrinsicTableProperty);

							targetTable.Properties.Insert(sourceTable.Properties.IndexOf(stp), itp);
						}
						else
						{
							ForeignKeyTableProperty fktp = CreateForeignKeyTableProperty(targetTable, stp as ForeignKeyTableProperty);

							targetTable.Properties.Insert(sourceTable.Properties.IndexOf(stp), fktp);
						}
					}
					else
					{
						// converge
						ConvergeTableProperties(targetTable, targetTable.Properties, tp, stp);
					}
				}
			}
			#endregion
			#endregion
		}

		private static IntrinsicTableProperty CreateIntrinsicTableProperty(Table targetTable, IntrinsicTableProperty sitp)
		{
			IntrinsicTableProperty itp = new IntrinsicTableProperty(sitp.PropertyName, targetTable, sitp.ColumnName);

			itp.AutoGeneratedOnInsert = sitp.AutoGeneratedOnInsert;
			itp.BaseClassPK = sitp.BaseClassPK;
			itp.DataLength = sitp.DataLength;
			itp.DataType = sitp.DataType;
			itp.DefaultValue = sitp.DefaultValue;
			itp.FieldName = sitp.FieldName;

			// need to handle foreign key back references

			itp.Nullable = sitp.Nullable;
			return itp;
		}

		private void ConvergeTableProperties(Table targetTable, List<TableProperty> propCollection, TableProperty tp, TableProperty stp)
		{
			if ( stp.Type == tp.Type )
			{
				// check for normal property changes.
				tp.AutoGeneratedOnInsert = stp.AutoGeneratedOnInsert;
				tp.DataLength = stp.DataLength;
				if ( tp.DataType != stp.DataType )
				{
					tp.DataType = stp.DataType;
					tp.DefaultValue = stp.DefaultValue;
				}

				tp.Nullable = stp.Nullable;
			}
			else
			{
				if ( stp.Type == TablePropertyType.Intrinsic )
				{
					// if stp is Intrinsic, then
					// tp must be a ForeignKey
					// so convert tp to a foreign key
					ForeignKeyTableProperty tfktp = tp as ForeignKeyTableProperty;

					if ( tfktp != null )
					{
						IntrinsicTableProperty titp = tfktp.ConvertToIntrinsic();
						propCollection.Insert(propCollection.IndexOf(tp), titp);

						propCollection.Remove(tp);
					}
				}
				else if ( stp.Type == TablePropertyType.ForeignKey )
				{
					// if source is a foreign key,
					// then tp must be intrinsic,
					// so create a foreignkey, and 
					// make sure that all the required 
					// methods are there
					ForeignKeyTableProperty sfktp = stp as ForeignKeyTableProperty;

					ForeignKeyTableProperty fktp = CreateForeignKeyTableProperty(targetTable, sfktp);

					propCollection.Insert(propCollection.IndexOf(tp), fktp);

					propCollection.Remove(tp);
				}
			}
		}

		private ForeignKeyTableProperty CreateForeignKeyTableProperty(Table targetTable, ForeignKeyTableProperty sfktp)
		{
			ForeignKeyTableProperty fktp = new ForeignKeyTableProperty(sfktp.PropertyName, targetTable, sfktp.ColumnName);

			fktp.AutoGeneratedOnInsert = sfktp.AutoGeneratedOnInsert;
			fktp.DataLength = sfktp.DataLength;
			fktp.DataType = sfktp.DataType;
			fktp.DefaultValue = sfktp.DefaultValue;
			fktp.FieldName = sfktp.FieldName;
			fktp.ForeignFieldName = sfktp.ForeignFieldName;
			fktp.ForeignKeyColumnName = sfktp.ForeignKeyColumnName;

			fktp.ForeignKeyTable = GetTableOrTemp(sfktp.ForeignKeyTable);

			fktp.ForeignObjectFieldName = sfktp.ForeignObjectFieldName;
			fktp.ForeignObjectPropertyName = sfktp.ForeignObjectPropertyName;
			fktp.ForeignPropertyName = sfktp.ForeignPropertyName;

			fktp.ForeignRetrieve = CopyMethod(sfktp.ForeignRetrieve, fktp.ForeignKeyTable);

			fktp.IgnoreForeign = sfktp.IgnoreForeign;
			fktp.IgnoreReference = sfktp.IgnoreReference;
			fktp.Nullable = sfktp.Nullable;
			fktp.PropertyName = sfktp.PropertyName;
			fktp.ReferenceColumnName = sfktp.ReferenceColumnName;
			fktp.ReferenceFieldName = sfktp.ReferenceFieldName;
			fktp.ReferenceObjectFieldName = sfktp.ReferenceObjectFieldName;
			fktp.ReferenceObjectPropertyName = sfktp.ReferenceObjectPropertyName;
			fktp.ReferencePropertyName = sfktp.ReferencePropertyName;

			fktp.ReferenceTable = GetTableOrTemp(sfktp.ReferenceTable);

			fktp.ReferenceRetrieve = CopyMethod(sfktp.ReferenceRetrieve, fktp.ReferenceTable);

			return fktp;
		}

		private Method CopyMethod(Method method, Table targetTable)
		{
			Method ret = null;

			if ( !(targetTable is TempTable) )
			{
				// see if the method already exists
				foreach ( Method meth in targetTable.Methods )
				{
					if ( method.MethodName == meth.MethodName )
					{
						ret = meth;
						break;
					}
				}
			}

			if ( ret == null )
			{
				ret = new Method(method.MethodName, GetTableOrTemp(method.Table), method.Type);

				ret.StoredProcedureName = method.StoredProcedureName;

				foreach ( MethodParameter mp in method.Parameters )
				{
					TableProperty tp = null;

					if ( mp.Property is PrimaryKeyTableProperty )
						tp = targetTable.GetPrimaryKey();
					else
						tp = GetTablePropertyOrTemp(mp.Property, targetTable);

					MethodParameter newMp = new MethodParameter(mp.ParameterName, ret, tp);

					ret.Parameters.Add(newMp);
				}

				targetTable.Methods.Add(ret);
			}

			return ret;
		}

		private TableProperty GetTablePropertyOrTemp(TableProperty tableProperty, Table targetTable)
		{
			TableProperty tp = targetTable.Properties.FindPropertyByColumnName(tableProperty.ColumnName);

			if ( tp != null )
				return tp;

			return new TempTableProperty(tableProperty);
		}

		private Table GetTableOrTemp(Table table)
		{
			Table tTable = _originalProject.Tables[table.TableName];

			if ( tTable != null )
				return tTable;

			return new TempTable(table);
		}


	}
}
